記得剛進公司的時候,遇到 Proc 或 lambda 都會很疑惑,為什麼可以這樣用,為什麼不直接使用 block就好?直到越看越多資料後,才漸漸瞭解他們的區別跟用途。
Block 是程式碼區塊,不是物件不能單獨存在,也不能指定給變數。
所以 Block 一定會跟隨在方法後面。 Ruby 的方法就像是其他語言的函式,那 block 就是函式內部的函式
。
一般來說我們把參數帶入函式,由函式決定要怎麼處理這個參數,有了block,我們讓參數也有邏輯。
# i 是 block 的參數
[1, 2, 3].map { |i| p "這個矩陣依序是:#{i}" }
# 等同:
[1, 2, 3].map do |i|
p "這個矩陣依序是:#{i}"
end
# 都會印出
"這個矩陣依序是:1"
"這個矩陣依序是:2"
"這個矩陣依序是:3"
# 回傳 ["這個矩陣依序是:1", "這個矩陣依序是:2", "這個矩陣依序是:3"]
用 {} 好,還是用 do ... end 好?
=> 如果 block 內容簡單,一行可以寫完,會用 { }。
在方法內部看到 yield
,就是要把控制權讓出來,給外面的block
def hello_block(word)
p "#{word}開始"
yield
p "#{word}結束"
end
hello_block('hello') { p 'block在此' }
# "hello開始"
# "block在此"
# "hello結束"
可以看到block沒有被視為參數,只有word
才是參數。
如果沒有給 block,會噴錯: LocalJumpError: no block given (yield)
如果不是每次都給block,可以用 block_give?
這個方法,有 block 才執行 yield
def hello_block(word)
p "#{word}開始"
yield if block_given?
p "#{word}結束"
end
hello_block('hello')
# "hello開始"
# "hello結束"
如果沒有 yield,呼叫方法時block有給跟沒給一樣
'hello'.split('l')
# ["he", "", "o"]
'hello'.split('l') { p 'block' }
# ["he", "", "o"]
def filter_number(array)
p "參數是#{array}"
result = []
array.each do |i|
result << i if yield(i)
end
result
end
filter_number([*1..10]) { |i| i % 2 == 0}
# "參數是[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"
#=> [2, 4, 6, 8, 10]
filter_number([*1..10]) { |i| i % 3 == 0}
# "參數是[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"
# => [3, 6, 9]
因為 block 最後一行會作為回傳值,當回傳 true 才把參數塞進篩選結果
等等,前面不是說 block 不能當參數嗎?為什麼又可以了?不是我誆你,是使用了&
轉換。前面的範例在方法的參數列看不出來需不需要 block,因為他們是implicite block
,現在要主動把 block 放進參數。
def email_customer(email_address, &log)
puts "I will email #{email_address}."
if block_given?
log.call
end
# code to execute ...
puts "I've emailed #{email_address}."
end
email_customer('a@c.com') { p "log the email record" }
# I will email a@c.com.
# "log the email record"
# I've emailed a@c.com.
透過&
,把傳進來的log 轉換成 proc,這樣一來就可以在方法內用call
執行 log 這個 proc。
因為有if block_given?
,如果沒有給 log 也不會錯
email_customer('a@c.com')
# I will email a@c.com.
# I've emailed a@c.com.
題外話,用implicite block 會比 block 轉換 proc 效能更好一點。
proc 是 Proc class的實體,proc 可以指定給變數存起來,讓傳進方法的程式碼區塊更有變化性。
proc = proc { |i| "This is #{i} times"}
#=> #<Proc:0x00007ffbc9c29a30@(pry):174>
proc = Proc.new { |i| "This is #{i} times"}
#=> #<Proc:0x00007ffbc9bc1b60@(pry):176>
存起來的 proc 就可以拿來使用
[1,2,3].map(&proc)
# ["This is 1 times", "This is 2 times", "This is 3 times"]
&:
&:
,就是使用了Symbol的 #to_proc 方法
:to_s.to_proc
# => #<Proc:0x00007ffbc47282e8(&:to_s)>
以前不懂的.map(&:method)
,原理就是 &
自動呼叫了 Symble 的 # to_proc方法,讓 symbol 被轉換成 proc 再代入
[1,2,3].map(&:to_s)
# ["1", "2", "3"]
# 效果等同:
[1,2,3].map {|i| i.to_s }
to_proc
原始碼大概長這樣
class Symbol
def to_proc
Proc.new { |i| i.send(self) }
end
end
proc 直接作為參數的一部分,並用.call
呼叫並輸入參數
def calculation(a, b, operation)
operation.call(a, b)
end
puts calculation(5, 6, proc { |a, b| a + b })
# 11
第一次學Ruby,我連這個字都不會念。kk音標是[ˋlæmdə],其實就是物理學的波長λ
。光速公式:c(光速)=λ(波長)×ν(頻率)
lambda 跟 proc 幾乎相等,但是 lambda 更像是 ruby 的 方法。
lambda 的兩種寫法:
lambda = lambda { |a, b| a + b }
# => #<Proc:0x00007ffbc2e4dc28@(pry):208 (lambda)>
# 等同:
lambda = ->(a,b) { a + b } # 參數放外面
lambda.class
# => Proc # 跟 proc 一樣是 Proc 的實體
proc 不檢查參數數目,沒帶入的參數會用nil取代,lambda 參數數目必須符合
proc = proc { |a, b| puts "first param: #{a},second param: #{b}" }
proc.call 1
# first param: 1,second param:
lambda = lambda { |a, b| puts "first param: #{a},second param: #{b}" }
lambda.call 1
# ArgumentError: wrong number of arguments (given 1, expected 2)
當 lambda 內執行到 return ,控制權還是在呼叫它的方法,而繼續執行該方法後的片段;但當 Proc 內使用到 return 時,不會回到呼叫它的方法,而是立即跳出該方法:
def proc_return
p 'first line'
proc = Proc.new { return 10}
"return value #{proc.call}"
end
proc_return
# "first line"
#=> 10
def lambda_return
p 'first line'
lambda = -> { return 10}
"return value #{lambda.call}"
end
lambda_return
# "first line"
# => "return value 10"
所以說 lambda 更像ruby 方法,執行完繼續往下執行而不是跳出該方法
參考資料